/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2012 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* http://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package org.easysoa.registry.rest.client;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.ws.rs.Consumes;
import javax.ws.rs.CookieParam;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.MatrixParam;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import com.sun.jersey.api.client.GenericType;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.WebResource.Builder;
import com.sun.jersey.api.representation.Form;
import com.sun.jersey.core.util.MultivaluedMapImpl;
/**
* Factory for client-side representation of a resource.
* See the <a href="package-summary.html">package overview</a>
* for an example on how to use this class.
*
* @author Martin Matula (martin.matula at oracle.com)
*
* Adapted for JAX-RS 1 by @author mkalam-alami
*/
public final class WebResourceFactory implements InvocationHandler {
private final WebResource target;
private final MultivaluedMap<String, String> headers;
private final List<Cookie> cookies;
private final Form form;
private static final MultivaluedMap<String, String> EMPTY_HEADERS = new MultivaluedMapImpl();
private static final Form EMPTY_FORM = new Form();
/**
* Creates a new client-side representation of a resource described by
* the interface passed in the first argument.
*
* Calling this method has the same effect as calling {@code WebResourceFactory.newResource(resourceInterface, rootTarget, false)}.
*
* @param <C> Type of the resource to be created.
* @param resourceInterface Interface describing the resource to be created.
* @param target WebResource pointing to the resource or the parent of the resource.
* @return Instance of a class implementing the resource interface that can
* be used for making requests to the server.
*/
public static <T> T newResource(Class<T> resourceInterface, WebResource target) {
return newResource(resourceInterface, target, false, EMPTY_HEADERS,
Collections.<Cookie>emptyList(), EMPTY_FORM);
}
/**
* Creates a new client-side representation of a resource described by
* the interface passed in the first argument.
*
* @param <C> Type of the resource to be created.
* @param resourceInterface Interface describing the resource to be created.
* @param target WebResource pointing to the resource or the parent of the resource.
* @param ignoreResourcePath If set to true, ignores path annotation on the resource interface (this is used when creating sub-resources)
* @param headers Header params collected from parent resources (used when creating a sub-resource)
* @param cookies Cookie params collected from parent resources (used when creating a sub-resource)
* @param form Form params collected from parent resources (used when creating a sub-resource)
* @return Instance of a class implementing the resource interface that can
* be used for making requests to the server.
*/
@SuppressWarnings("unchecked")
public static <C> C newResource(Class<C> resourceInterface, WebResource target, boolean ignoreResourcePath,
MultivaluedMap<String, String> headers, List<Cookie> cookies, Form form) {
return (C) Proxy.newProxyInstance(resourceInterface.getClassLoader(),
new Class[] {resourceInterface},
new WebResourceFactory(ignoreResourcePath ? target : addPathFromAnnotation(resourceInterface, target),
headers, cookies, form));
}
private WebResourceFactory(WebResource target, MultivaluedMap<String, String> headers, List<Cookie> cookies,
Form form) {
this.target = target;
this.headers = headers;
this.cookies = cookies;
this.form = form;
}
@Override
@SuppressWarnings("unchecked")
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// get the interface describing the resource
Class<?> proxyIfc = proxy.getClass().getInterfaces()[0];
// response type
Class<?> responseType = method.getReturnType();
// determine method name
String httpMethod = getHttpMethodName(method);
if (httpMethod == null) {
for (Annotation ann : method.getAnnotations()) {
httpMethod = getHttpMethodName(ann.annotationType());
if (httpMethod != null) {
break;
}
}
}
// create a new UriBuilder appending the @Path attached to the method
WebResource newTarget;
Path p = method.getAnnotation(Path.class);
if (p != null && !p.value().contains("{")) {
newTarget = addPathFromAnnotation(method, target);
}
else {
newTarget = target;
}
if (httpMethod == null) {
if (newTarget == target) {
// no path annotation on the method -> fail
throw new UnsupportedOperationException("Not a resource method.");
} else if (!responseType.isInterface()) {
// the method is a subresource locator, but returns class,
// not interface - can't help here
throw new UnsupportedOperationException("Return type not an interface");
}
}
// process method params (build maps of (Path|Form|Cookie|Matrix|Header..)Params
// and extract entity type
MultivaluedMap<String, String> headers = new MultivaluedMapImpl();
headers.putAll(this.headers);
LinkedList<Cookie> cookies = new LinkedList<Cookie>(this.cookies);
Form form = new Form();
form.putAll(this.form);
Annotation[][] paramAnns = method.getParameterAnnotations();
Object entity = null;
Type entityType = null;
for (int i = 0; i < paramAnns.length; i++) {
Map<Class<?>, Annotation> anns = new HashMap<Class<?>, Annotation>();
for (Annotation ann : paramAnns[i]) {
anns.put(ann.annotationType(), ann);
}
Annotation ann;
Object value = args[i];
if (anns.isEmpty()) {
entityType = method.getGenericParameterTypes()[i];
entity = value;
} else {
if (value == null && (ann = anns.get(DefaultValue.class)) != null) {
value = ((DefaultValue) ann).value();
}
if (value != null) {
if ((ann = anns.get(PathParam.class)) != null) {
newTarget = newTarget.path((String) value); // XXX
//newTarget = newTarget.pathParam(((PathParam) ann).value(), (String) value);
} else if ((ann = anns.get((QueryParam.class))) != null) {
newTarget = newTarget.queryParam(((QueryParam) ann).value(), (String) value);
} else if ((ann = anns.get((HeaderParam.class))) != null) {
headers.put(((HeaderParam) ann).value(), (List<String>) value);
} else if ((ann = anns.get((CookieParam.class))) != null) {
String name = ((CookieParam) ann).value();
Cookie c;
if (!(value instanceof Cookie)) {
c = new Cookie(name, value.toString());
} else {
c = (Cookie) value;
if (!name.equals(((Cookie) value).getName())) {
// is this the right thing to do? or should I fail? or ignore the difference?
c = new Cookie(name, c.getValue(), c.getPath(), c.getDomain(), c.getVersion());
}
}
cookies.add(c);
} else if ((ann = anns.get((MatrixParam.class))) != null) {
throw new UnsupportedOperationException();
//newTarget = newTarget.matrixParam(((MatrixParam) ann).value(), value);
} else if ((ann = anns.get((FormParam.class))) != null) {
form.add(((FormParam) ann).value(), value.toString());
}
}
}
}
if (httpMethod == null) {
// the method is a subresource locator
return WebResourceFactory.newResource(responseType, newTarget, true, headers, cookies, form);
}
// accepted media types
Produces produces = method.getAnnotation(Produces.class);
if (produces == null) {
produces = proxyIfc.getAnnotation(Produces.class);
}
String[] accepts = produces == null ? null : produces.value();
// determine content type
String contentType = null;
if (entity != null) {
Consumes consumes = method.getAnnotation(Consumes.class);
if (consumes == null) {
consumes = proxyIfc.getAnnotation(Consumes.class);
}
if (consumes != null && consumes.value().length > 0) {
// TODO: should consider q/qs instead of picking the first one
contentType = consumes.value()[0];
}
}
Builder b;
if (accepts != null) {
b = newTarget.accept(accepts);
} else {
b = newTarget.getRequestBuilder();
}
// apply header params and cookies
for (Cookie c : cookies) {
b = b.cookie(c);
}
// TODO: change this to b.headers(headers) once we switch to the latest JAX-RS API
for (Map.Entry<String, List<String>> header : headers.entrySet()) {
for (Object value : header.getValue()) {
b = b.header(header.getKey(), value);
}
}
Object result;
if (entity == null && !form.isEmpty()) {
entity = form;
contentType = MediaType.APPLICATION_FORM_URLENCODED;
} else {
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
if (!form.isEmpty()) {
if (entity instanceof Form) {
((Form) entity).putAll(form);
} else {
// TODO: should at least log some warning here
}
}
}
GenericType<?> responseGenericType = new GenericType<Object>(method.getGenericReturnType());
if (entity != null) {
if (entityType instanceof ParameterizedType) {
entity = new GenericEntity<Object>(entity, entityType);
}
b.entity(entity, contentType);
result = b.method(httpMethod, responseGenericType);
} else {
result = b.method(httpMethod, responseGenericType);
}
return result;
}
private static WebResource addPathFromAnnotation(AnnotatedElement ae, WebResource target) {
Path p = ae.getAnnotation(Path.class);
if (p != null) {
target = target.path(p.value());
}
return target;
}
private static String getHttpMethodName(AnnotatedElement ae) {
HttpMethod a = ae.getAnnotation(HttpMethod.class);
return a == null ? null : a.value();
}
}